import datetime
import time
import random
import socket

import webview
import requests
from pathlib import Path
import pandas as pd
import os
import ctypes
import subprocess
from apscheduler.schedulers.background import BackgroundScheduler
import yaml

"""
打包请在纯净的python环境中打包，否则会包含当前python运行环境中的所有库，导致包体积非常大
打包方法：
1. auto-py-to-exe
2. 选择One File，选择Window Based(hide the console) 
3. 添加图标为stocks.icon
4. 添加Additional Files-> Add Folder, 选择本目录下的dist文件夹，确定
5. 高级里设置--name为stocks
6. upx-dir选择本路径下的upx，以压缩生成的exe文件

pyinstaller --noconfirm --onefile --windowed --icon "E:\\repos\\gravity\\stocks\\stocks.ico" --name "stocks" --upx-dir "E:\\repos\\gravity\\stocks\\upx" --add-data "E:\\repos\\gravity\\stocks\\dist;dist/"  "E:\\repos\\gravity\\stocks\\main.py"
"""
window: None | webview.Window = None


def run_as_admin(cmd):
    if os.name != 'nt':
        raise Exception("This function is only supported on Windows.")

    # Check if the script is already running as admin
    if ctypes.windll.shell32.IsUserAnAdmin():
        # If already admin, just run the command
        subprocess.run(cmd, shell=True)
    else:
        # If not admin, create a new process with admin privileges
        ctypes.windll.shell32.ShellExecuteW(
            None, "runas", "cmd.exe", f"/C {cmd}", None, 1)


def get_latest_release_download_url(owner, repo):
    """
    获取某个仓库最新的发行版的下载链接
    :param owner:
    :param repo:
    :return:
    """
    # Gitee API URL for releases
    api_url = f"https://gitee.com/api/v5/repos/{owner}/{repo}/releases"

    try:
        response = requests.get(api_url)
        response.raise_for_status()  # Raise an error for bad responses
        releases = response.json()

        if not releases:
            print("No releases found.")
            return None

        # Sort releases by published_at to get the latest one
        latest_release = sorted(
            releases, key=lambda x: x['created_at'], reverse=True)[0]

        # Get the download URL of the first asset (assuming there is at least one asset)
        assets = latest_release.get('assets', [])
        if not assets:
            print("No assets found in the latest release.")
            return None

        download_url = assets[0].get('browser_download_url')
        if not download_url:
            print("Download URL not found.")
            return None

        return download_url

    except requests.exceptions.RequestException as e:
        print(f"An error occurred: {e}")
        return None


def download(url):
    """下载文件到services目录"""
    from pathlib import Path
    import requests

    try:
        # 创建services目录
        services_dir = Path('services')
        services_dir.mkdir(parents=True, exist_ok=True)

        # 获取文件名
        filename = url.split('/')[-1]
        file_path = services_dir / filename

        # 流式下载大文件
        with requests.get(url, stream=True) as r:
            r.raise_for_status()
            total_size = int(r.headers.get('content-length', 0))

            # 进度跟踪
            print(f"开始下载: {filename} ({total_size/1024/1024:.1f}MB)")
            downloaded = 0

            with open(file_path, 'wb') as f:
                for chunk in r.iter_content(chunk_size=8192):
                    f.write(chunk)
                    downloaded += len(chunk)
                    # 打印下载进度
                    if total_size > 0:
                        progress = downloaded / total_size * 100
                        print(f"\r下载进度: {progress:.1f}%", end='', flush=True)

        print(f"\n文件已保存到: {file_path}")
        return str(file_path)

    except Exception as e:
        print(f"下载失败: {str(e)}")
        raise


def start_damo(port):
    exe_path = 'WebDM.exe'
    cmd = f'"{exe_path}" --port {port}'
    run_as_admin(cmd)


def get_unused_port():
    while True:
        port = random.randint(1024, 65535)
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            sock.bind(("127.0.0.1", port))
            sock.close()
            return port
        except OSError:
            pass


def change_active_window_content():
    active_window = webview.active_window()
    if active_window:
        active_window.load_html('<h1>You changed this window!</h1>')


def click_me():
    active_window = webview.active_window()
    if active_window:
        active_window.load_html('<h1>You clicked me!</h1>')


def do_nothing():
    pass


def deep_merge(dict1, dict2):
    """
    嵌套合并两个字典

    :param dict1:
    :param dict2:
    :return:
    """
    result = dict1.copy()
    for key, value in dict2.items():
        if key in result and isinstance(result[key], dict) and isinstance(value, dict):
            result[key] = deep_merge(result[key], value)
        else:
            result[key] = value
    return result


def say_this_is_window_2():
    active_window = webview.active_window()
    if active_window:
        active_window.load_html('<h1>This is window 2</h2>')


def open_file_dialog():
    active_window = webview.active_window()
    active_window.create_file_dialog(
        webview.SAVE_DIALOG, directory='/', save_filename='test.file')


def custom_logic(window):
    window.toggle_fullscreen()
    window.evaluate_js('alert("Nice one brother")')


class Api:
    def __init__(self):
        # 定义 settings.yaml 文件的路径
        self.settings_file = os.path.join(
            os.path.dirname(__file__), 'settings.yaml')

        self.host = 'http://sges.yangke.site'
        self.port = 5002
        self.folder = Path('stocks')
        self.daily_path = self.folder.joinpath('daily')
        if not self.daily_path.exists():
            self.daily_path.mkdir(parents=True, exist_ok=True)
        self.holiday_file = self.folder.joinpath('holidays.csv')
        self.holidays: pd.DataFrame | None = None
        self.stocks_list = self.folder.joinpath('stocks_list.csv')
        self.day_prefix = 'daily'
        self.daily_cols = ['TradeDate', 'Open', 'High', 'Low', 'Close', 'Amount', 'Vol', '市值',
                           '流通市值', '市净率', '市盈率', '换手率']
        self.mysql: dict | None = None
        self.mute = False  # 是否不打印输出信息
        self.scheduler: BackgroundScheduler | None = BackgroundScheduler(
            timezone='Asia/Shanghai')
        if self.mysql is not None:
            ...

    def read_settings(self):
        """读取 settings.yaml 文件"""
        try:
            if os.path.exists(self.settings_file):
                with open(self.settings_file, 'r', encoding='utf-8') as file:
                    settings = yaml.safe_load(file) or {}  # 可以直接传递对象给js
                    return settings
            return {}
        except Exception as e:
            print(f'读取文件失败: {e}')
            return {}

    def save_settings(self, settings):
        """保存 settings.yaml 文件"""
        try:
            settings1 = {}
            if os.path.exists(self.settings_file):
                with open(self.settings_file, 'r', encoding='utf-8') as file:
                    settings1 = yaml.safe_load(file) or {}  # 可以直接传递对象给js
            settings1 = deep_merge(settings1, settings)  # 合并两个字典，会嵌套合并
            with open(self.settings_file, 'w', encoding='utf-8') as file:
                yaml.safe_dump(settings1, file)
            return True
        except Exception as e:
            print(f'保存文件失败: {e}')
            return False

    def start_damo(self, reg_code, add_code, port=8765):
        """启动WebDM.exe"""
        if not os.path.exists('services/WebDM.exe'):
            # 下载WebDM.exe并放到当前目录下的services目录下
            url = get_latest_release_download_url('yangke02', 'webdm')
            download(url)
        elif os.path.exists('services/WebDM.exe'):
            cmd = f'"WebDM.exe" --port {port}'
            run_as_admin(cmd)

    def quit(self):
        import sys
        self.print_to_term('即将退出...')
        window.destroy()
        sys.exit(0)

    def print_to_term(self, msg: str, kind='info'):
        """

        :param msg:
        :param kind: 可取值warning info success error system
        :return:
        """
        global window
        if self.mute:
            return
        if isinstance(window, webview.Window):
            msg = f"{datetime.datetime.now().strftime('%Y%m%d %H:%M:%S')}>{msg}"
            cmd = f'window.debug("{msg}", "{kind}")'
            window.evaluate_js(
                cmd, callback=None)

    def set_task_bar_content(self, msg: str, process: float = 0):
        """

        :param msg:
        :param kind: 可取值warning info success error system
        :return:
        """
        global window
        if isinstance(window, webview.Window):
            cmd = f'window.setTaskBarContent("{msg}", "{process}")'
            window.evaluate_js(
                cmd, callback=None)

    def printWindow(self):
        global window
        self.print_to_term(window.__class__)

    def needUpdate(self, data_file: str | datetime.datetime | Path):
        """
        判断文件是否需要更新
        :param data_file:
        :return:
        """
        """
        检查数据文件是否需要更新；如果传入的是时间，则检查该时间之后是否有股票数据更新。
        当上一个工作日4点以后更新过，且今天时间不到下午4点，则无需更新

        :param data_file:
        :return:
        """
        if isinstance(data_file, datetime.datetime):
            last_change_time = data_file
            last_change_date_str = last_change_time.strftime("%Y-%m-%d")
        elif data_file is None:  # 如果文件为空
            return True
        elif os.path.exists(data_file):  # 如果文件存在，判断文件是否需要更新
            last_change_time = os.stat(data_file).st_mtime  # float类型，表示秒数
            last_change_time = datetime.datetime.fromtimestamp(
                last_change_time)
            last_change_date_str = last_change_time.strftime("%Y-%m-%d")
        else:  # 文件不存在，则需要更新
            return True

        now_time = datetime.datetime.now()  # 当前时间
        now_time_date_str = now_time.strftime("%Y-%m-%d")  # 用来和最后修改日期作比较

        if last_change_date_str == now_time_date_str:  # 如果是今天修改的
            if last_change_time.hour > 15:  # 最后修改日期是当天15点以后，则不用更新数据
                self.print_to_term(f"数据文件{data_file}已经最新，无需更新！")
                return False  # 无需更新数据
            else:
                if now_time.hour < 16:  # 如果今天修改过且今天还没到16:00，也无须更新
                    return False
                # 如果今天时holiday
                if self.is_holiday(datetime.date.today()):  # 如果今天时休息日，今天更新过也不用更新
                    self.print_to_term(f"数据文件{data_file}已经最新，无需更新！")
                    return False
                else:
                    return True
        else:
            last_working_day_str = str(
                self.get_previous_working_day(include_time=True)) + ' 16:00:00'
            last_working_datetime = datetime.datetime.strptime(
                last_working_day_str, '%Y-%m-%d %H:%M:%S')
            if last_change_time > last_working_datetime:
                # 如果最后修改日期在上一个工作日15点之后
                if isinstance(data_file, str):
                    self.print_to_term(f"数据文件{data_file}已经最新，无需更新！{now_time=}")
                else:
                    self.print_to_term(
                        f"数据文件更新于{data_file}，无需更新！{now_time=}")
                return False  # 无需更新数据
            else:
                return True

    def get_previous_working_day(self, last_datetime=None, include_time=False) -> datetime.date:
        """
        获得指定时间点最近的一个交易日，包含今天。

        :param last_datetime: 指定的时间点
        :param include_time: 是否判断时间，如果includeTime=True，则会进一步判断今天的时间，如果时间在下午4:00之前，则不包括今天，因为当天的股票数据还没整理出来
        :return: 最近一个交易日日期
        """
        if last_datetime is None:  # 不能直接在定义函数时给初值，否则初值将在函数第一次运行时就确定，后续无论运行多少次，都不会改变
            last_datetime = datetime.datetime.now()
        if last_datetime.date() > datetime.date.today():
            self.print_to_term("因为未来的股票数据不存在，不能获得将来日期的前一个工作日！", "warning")
        if self.is_holiday(last_datetime.date()):
            last_date = last_datetime.date()
            one_day = datetime.timedelta(days=1)
            pre_day = last_date - one_day
            while self.is_holiday(pre_day):
                pre_day = pre_day - one_day
            return pre_day
        else:
            if include_time:
                hour = last_datetime.hour
                if hour < 16:  # 额外对时间进行判断
                    return self.get_working_day_before_day(last_datetime.date())
                else:
                    return last_datetime.date()
            return last_datetime.date()

    def get_working_day_before_day(self, day_datetime: None | datetime.date,
                                   day_num: int = 1) -> datetime.date | None:
        """
        获得指定日期的前一个交易日或后一个交易日，需要注意的是，该函数不对指定日期是否为工作日进行判断，
        因此，获得最近一个交易日，不能使用get_working_day_before_day(date.today(),1)
        :param day_datetime: datetime类型，默认为今天
        :param day_num: 可取正整数和-1，前day_num个交易日，取-1时,表示指定日期的后一个交易日，不可以取其他负值
        :return: date_datetime】,-----返回对应交易日的datetime类型数据
        """
        if day_datetime is None:
            day_datetime = datetime.date.today()
        if day_num < -1:
            self.print_to_term("day_num不能取-1以外的负值（{}）".format(day_num))
            return
        if day_num == 1 or day_num == -1:
            one_day = datetime.timedelta(days=1)
            if day_num == -1:
                pre_day = day_datetime + one_day
            else:
                pre_day = day_datetime - one_day
            while self.is_holiday(pre_day):
                if day_num == -1:
                    pre_day = pre_day + one_day
                else:
                    pre_day = pre_day - one_day
            return pre_day
        else:  # day_num为大于1的整数
            day_datetime = self.get_working_day_before_day(
                day_datetime, 1)  # 计算上一个交易日
            # 返回上一个交易日的上day_num-1个交易日
            return self.get_working_day_before_day(day_datetime, day_num - 1)

    def is_holiday(self, day_datetime: datetime.date = datetime.date.today()):
        """
        判断今天是不是休市，只适用于国内股市，不包括港股

        :param day_datetime: 给定日期的datetime类型
        :return: True 或 False
        """
        day_str = day_datetime.strftime("%Y-%m-%d")
        if (day_datetime.weekday() == 5) or (day_datetime.weekday() == 6):  # 周六和周日肯定是休市的，这里的5、6分别代表周六和周日
            return True
        date_str = day_str
        if self.holidays is None:
            self.holidays = pd.read_csv(self.holiday_file)
            if day_str not in self.holidays['Date'].tolist():
                self.downloadHolidays()  #
                self.holidays = pd.read_csv(self.holiday_file)
        is_open = self.holidays[self.holidays['Date']
                                == date_str]['Open'].item()
        is_open = bool(is_open)  # 1：True，0：False
        return not is_open  # python会自动将0转换成False，1转换成True

    def downloadHolidays(self):
        # 不能检查本地文件是否最新，因为检查本地文件是否最新需要使用到假日文件，会造成循环调用
        if self.holidays is None:
            self.holidays = pd.read_csv(self.holiday_file)
        day_str = datetime.datetime.now().strftime("%Y-%m-%d")
        if day_str in self.holidays['Date'].tolist():  # 如果假日表中没有今天，则更新
            self.print_to_term(f"{self.holiday_file.as_posix()}已经最新，无需更新！")
            return True
        self.print_to_term(f"开始下载节假日数据...")
        res = requests.get(f"{self.host}:{self.port}/?Action=downloadHolidays")
        if res.status_code == 200:
            data = res.json()
            df = pd.DataFrame(data)
            df['Date'] = pd.to_datetime(df['Date'])
            if self.folder:
                df.to_csv(self.holiday_file, index=False)

            if self.mysql:
                ...
        self.print_to_term(
            f"节假日数据下载完成->{self.holiday_file.as_posix()}。", "success")

    def downloadLastDay(self, tries=1):
        """
        下载最后一天的股票日线数据，不复权。相当于点击【所有股票日线数据】
        :return:
        """
        try:
            res = requests.get(
                f"{self.host}:{self.port}/?Action=downloadLastDay")
            if res.status_code == 200:
                data = res.json()
                df = pd.DataFrame(data)
                df.rename(columns={'T': 'TradeDate',
                                   'O': 'Open',
                                   'H': 'High',
                                   'L': 'Low',
                                   'P': 'Close',
                                   "Cje": "Amount",
                                   "V": "Vol",
                                   "Sz": "市值",
                                   "Lt": "流通市值",
                                   "Sjl": "市净率",
                                   "Pe": "市盈率",
                                   "Hs": "换手率",
                                   "Dm": "symbol"}, inplace=True)
                return df
        except:
            if tries < 3:
                return self.downloadLastDay(tries + 1)

    def downloadStocksDailyAll(self):
        """
        下载所有股票的日线数据

        :return:
        """
        self.print_to_term(f"开始下载所有股票日线数据->{self.stocks_list.as_posix()}")
        if not self.daily_path.exists():  # 个别情况下，软件创建目录失败，这里总是重新尝试一下
            self.daily_path.mkdir(parents=True, exist_ok=True)
        self.print_to_term(f"1.检查本地数据")
        # self.mute = True
        self.downloadHolidays()
        self.print_to_term(f"1.1检查本地假期数据表完毕")
        self.downloadStocksTotalInfo()
        self.print_to_term(f"1.1检查本地股票列表完毕")
        # self.mute = False
        stock_list = pd.read_csv(self.stocks_list)
        num_total = len(stock_list)
        # 下载最后一天所有股票的日线数据
        data_last_day = self.downloadLastDay()

        for idx, row in stock_list.iterrows():
            symbol = f"{row.Symbol:06}"
            progress = f"{int(idx) + 1}/{num_total}"
            progress1 = (int(idx + 1) / num_total) * 100
            self.set_task_bar_content(
                f'正在下载股票{symbol}数据，下载进度：{progress1:5.2f}%', progress1)
            self.downloadStocksDailySingle(
                symbol, process=progress, last_daily_all=data_last_day)
        self.print_to_term("股票日线数据更新完成。", "success")
        self.set_task_bar_content(f'就绪')

    def get_last_modify_time(self, file):
        if os.path.exists(file):
            timestamp = os.path.getmtime(file)
            time = datetime.datetime.fromtimestamp(timestamp)
            return time
        else:
            return datetime.datetime(2000, 1, 1)

    def getJobs(self):
        if self.scheduler is None:
            return None
        jobs = self.scheduler.get_jobs()
        if len(jobs) == 0:
            self.print_to_term('当前不存在后台任务')
        else:
            for job in jobs:
                self.print_to_term(str(job))

    def autoUpdate(self):
        hour = 16
        minute = 0
        second = 0
        if self.scheduler is None:
            self.scheduler = BackgroundScheduler(timezone='Asia/Shanghai')
        self.scheduler.add_job(self.downloadStocksDailyAll, 'cron',
                               hour=hour, minute=minute, second=second, day='*')
        self.print_to_term(
            f"execute job every day：每天{hour}:{minute}:{second}更新当天日线数据", 'success')
        self.scheduler.start()

    def downloadStocksDailySingle(self, symbol, tries=1, process=None, last_daily_all=None):
        dst_path = self.daily_path.joinpath(f"{self.day_prefix}{symbol}.csv")
        if symbol == '000508':  # 代码为000508的股票没有数据，因为很早就退市了
            self.print_to_term(f'股票琼民源000508已退市，数据为空，...')
            return
        if process is None:
            self.print_to_term(f"开始下载股票{symbol}数据...")
        else:
            self.print_to_term(f"开始下载股票{symbol}数据({process})...")

        if last_daily_all is not None:
            # 等效的修改日期，如果文件是上一个工作日16点以后修改，则认为是上一个工作日，如果是上一个工作日16点以前修改，则认为是上上一个工作日
            modify_time_eqa = self.get_previous_working_day(
                self.get_last_modify_time(dst_path), include_time=True)
            last_date = self.get_previous_working_day(include_time=True)
            second_last_date = self.get_working_day_before_day(last_date)
            if modify_time_eqa == second_last_date:
                # 如果文件更新日期是上一个工作日，则从last_daily_all中获取数据，不在请求网络，请求网络耗时很高
                symbol_row: pd.DataFrame = last_daily_all[last_daily_all['symbol'] == symbol]
                if len(symbol_row) == 0:
                    ...  # 没有当前股票的数据，则单独下载
                else:
                    if pd.to_datetime(symbol_row['TradeDate']).item().date() == last_date:
                        row_list = [str(symbol_row[col].item())
                                    for col in self.daily_cols]
                        row_str = ",".join(row_list)
                        with open(dst_path, "a", encoding="utf-8") as f:
                            f.write(f"{row_str}\n")
                        self.print_to_term(
                            f"股票{symbol}下载完成->{dst_path.as_posix()}", "success")
                        return
                    else:
                        ...  # 日期不是想要的日期，则单独下载
            elif modify_time_eqa > second_last_date:
                self.print_to_term(
                    f"股票{symbol}数据已是最新->{dst_path.as_posix()}", "success")
                return
        try:
            res = requests.get(
                f"{self.host}:{self.port}/?Action=downloadStocksDailySingle&symbol={symbol}")
            if res.status_code == 200:
                data = res.json()
                df = pd.DataFrame(data)
                if len(df) == 0:
                    self.print_to_term(f'当前股票数据为空{symbol}...')
                    return
                df['TradeDate'] = pd.to_datetime(
                    df['TradeDate'], format='mixed')
                df = df[
                    ['TradeDate', 'Open', 'High', 'Low', 'Close', 'Amount', 'Vol', '市值', '流通市值', '市净率',
                     '市盈率',
                     '换手率']]
                if self.folder:
                    df.to_csv(dst_path, index=False, encoding='utf-8')
                if self.mysql:
                    ...
        except Exception as e:
            if tries < 3:  # 重新尝试一次，最多试三次
                self.print_to_term(f'下载出错，重新尝试({tries}/3)...')
                self.downloadStocksDailySingle(symbol, tries + 1)
        self.print_to_term(f"股票{symbol}下载完成->{dst_path.as_posix()}", "success")

    def downloadStocksTotalInfo(self):
        self.print_to_term(f'开始下载股票列表...')
        # 判断本地文件的修改时间
        time = self.get_last_modify_time(self.stocks_list)
        self.print_to_term(f"股票列表更新时间为{str(time)}")
        if not self.needUpdate(time):
            self.print_to_term(f'本地文件已经是最新->{self.stocks_list}')
            return
        res = requests.get(
            f"{self.host}:{self.port}/?Action=downloadStocksTotalInfo")
        if res.status_code == 200:
            data = res.json()
            df = pd.DataFrame(data)
            if self.folder:
                df.to_csv(self.stocks_list, index=False)
            if self.mysql:
                ...
        self.print_to_term(
            f"股票列表下载完成->{self.stocks_list.as_posix()}", "success")

    def login_exchange(self, username, password):  # 登录交易系统
        ...


def main():
    global window
    chinese = {
        'global.quitConfirmation': u'确定关闭?',
    }

    api = Api()
    window = webview.create_window(
        title='stocks',
        url='./dist/index.html',
        width=1300,
        height=900,
        resizable=True,  # 固定窗口大小
        text_select=True,  # 禁止选择文字内容
        confirm_close=True,  # 关闭时提示
        js_api=api,  # api中定义供html调用的函数
        min_size=(1000, 620)  # 设置窗口最小尺寸
    )

    #     webview.start(menu=menu_items)
    # 启动窗口
    # webview.start(localization=chinese, http_server=True, debug=True)
    # webview.start(custom_logic, window)  # 传入的函数会被立即执行
    webview.start(localization=chinese, ssl=True,
                  debug=True)  # 该语句会阻塞，直到程序关闭后才会继续执行后续代码


def test():
    api = Api()
    # api.downloadHolidays()
    # api.downloadStocksDailyAll()
    # api.downloadStocksDailySingle('000008')
    # api.downloadStocksTotalInfo()
    # api.autoUpdate()
    # api.getJobs()
    api.start_damo('mmdlljafd8c5cbf193e99306e9eb61ceb5bd44', 'ewdSEKFZP')


if __name__ == '__main__':
    main()
    # api = Api()
    # api.downloadStocksDailyAll()
